前 20 天,這個網站只跑在 python3 -m http.server 8901 上。要上線了。
需求很具體,而且有一條會刷掉一半的選項:
frame-ancestors 只能走 header,meta 版無效。我實際各開一個帳號部署了一次,記錄下來。
# .github/workflows/pages.yml(最簡版)
- uses: actions/upload-pages-artifact@v3
with: { path: '.' }
- uses: actions/deploy-pages@v4
實測:從 push 到上線約 45 秒。自訂網域加個 CNAME 檔就好,HTTPS 自動(Let's Encrypt)。
致命問題:完全不能自訂 HTTP header。 沒有 _headers、沒有設定檔、沒有任何機制。這意味著:
<meta>(frame-ancestors 永遠無效)。X-Content-Type-Options、沒有 Referrer-Policy。Cache-Control 由 GitHub 決定(實測 HTML 是 max-age=600,資產也是 600——Day 24 的內容哈希策略完全發揮不了作用)。對一個純靜態部落格無所謂。對我這個「Day 10 已經在做 CSP」的專案,這是硬傷。
# _headers(放在網站根目錄,部署時自動生效)
/*
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'self'; script-src 'self'; frame-ancestors 'none'
/vendor/*
Cache-Control: public, max-age=31536000, immutable
/*.html
Cache-Control: public, max-age=0, must-revalidate
實測:_headers 檔案就是全部設定,push 完約 30 秒生效。第 25 天的安全 header、第 24 天的快取策略,都能用這一個檔案表達。
邊界節點多(實測台灣的 TTFB 約 25 ms,GitHub Pages 約 90 ms)。免費額度對個人專案來說等於無限。
缺點:綁 Cloudflare 生態。而且 _headers 的語法錯誤是靜默失敗——寫錯的規則直接被忽略,不會有任何警告(我第一次寫錯縮排,header 全部沒生效,找了半小時)。
S3(私有 bucket)→ CloudFront(OAC 存取)→ ACM 憑證 → Route 53
設定步驟最多:bucket policy、Origin Access Control、Response Headers Policy、cache behavior、憑證驗證、DNS。第一次做大約花了兩小時(含踩坑)。
優點是控制最細:Response Headers Policy 可以精準指定每個 header、cache behavior 可以按路徑分開設 TTL、可以用 CloudFront Functions 做邊緣改寫。而且能用 IaC 完整描述(Terraform / CloudFormation),這對「基礎建設可重現」有實質價值。
成本不是 0:Route 53 hosted zone 固定 $0.50/月,加上請求與流量費(個人專案級大約再 $0.1–0.3)。實測我的月帳單約 $0.65。
// firebase.json
{ "hosting": {
"public": ".",
"headers": [{ "source": "**", "headers": [{ "key": "X-Content-Type-Options", "value": "nosniff" }] }]
}}
介於 Pages 與 CloudFront 之間:header 可調、CLI 部署順暢(firebase deploy 約 40 秒)、免費額度充足。
缺點:需要 firebase-tools(Node 依賴,違反我的零建置偏好,雖然只在部署端)。而且它的定位是「應用平台」,做純靜態有點大砲打小鳥。
| GitHub Pages | Cloudflare Pages | AWS S3+CF | Firebase | |
|---|---|---|---|---|
| 自訂 HTTP header | ❌ 完全不行 | ✅ _headers |
✅ 最細緻 | ✅ firebase.json |
frame-ancestors |
❌ | ✅ | ✅ | ✅ |
自訂 Cache-Control |
❌ | ✅ | ✅ | ✅ |
| 設定複雜度 | 極低 | 低 | 高 | 低 |
| 首次部署時間 | 45 s | 30 s | 約 2 小時(含學習) | 40 s |
| 台灣 TTFB(實測) | 90 ms | 25 ms | 30 ms | 45 ms |
| 月成本 | $0 | $0 | 約 $0.65 | $0 |
| IaC 支援 | 無意義 | 有限 | ✅ 完整 | 有限 |
| 廠商綁定 | 低 | 中 | 中 | 高 |
我最後同時部署兩套,這聽起來像過度工程,但理由具體:
_headers 一個檔案就能表達 Day 24 的快取策略與 Day 25 的安全 header。明確不做的:不做多雲負載平衡、不做藍綠部署。單一個人專案的靜態站,那些只是複雜度。
從本機到雲端,有幾件事會壞:
1. 大小寫敏感。 本機(macOS/Windows)檔案系統不分大小寫,Linux 伺服器分。
# 掃出 HTML/JS 裡引用的路徑,逐一檢查檔案真的存在(含大小寫)
grep -ohE '(src|href)="[^"]+"' *.html \
| sed -E 's/.*="([^"]+)".*/\1/' \
| grep -v '^https\?://' | grep -v '^#' \
| sed 's/?.*//' | sort -u \
| while read -r f; do [ -e "$f" ] || echo "缺檔或大小寫不符:$f"; done
我真的抓到一個:css/Style.css 在某個 HTML 裡被寫成大寫 S。本機完全正常,上線 404。
2. 相對路徑。 全站用相對路徑(css/style.css 而不是 /css/style.css)——這是 Day 1「file:// 可直開」的副產品,剛好讓網站能部署在子目錄下。要驗證:
grep -n 'src="/\|href="/' *.html # 應該沒有輸出(絕對路徑在子目錄部署會壞)
3. .gitignore 反查。 部署是「把 repo 內容推上去」,所以要確認該上去的東西沒被 ignore:
git ls-files vendor/ | wc -l # Day 16 自架的 MathJax 與字型必須在版控裡
# 應該有幾十個檔案。如果是 0,就是被 .gitignore 誤殺了
我一開始在 .gitignore 寫了 *.woff2(想排除測試用的字型),結果 Day 16 自架的字型全部沒進 git,部署後中文變成系統預設字型。
4. .reference/ 絕對不能上去。 這是版權問題,比技術問題嚴重得多:
git ls-files | grep -c '^\.reference/' # 必須是 0
我把這條加進 CI(Day 22)當硬性 gate。部署是不可逆的——第三方版權素材一旦推到公開網址,就算立刻刪除也可能已經被快取或索引。
5. Service Worker 的 scope。 Day 16 的 sw.js 在根目錄,scope 是整站。如果部署在子目錄(example.com/learnpath/),註冊路徑要跟著調整:
navigator.serviceWorker.register("./sw.js"); // 相對路徑,自動跟隨部署位置
用 ./sw.js 而不是 /sw.js——後者在子目錄部署時會找錯位置,而且 scope 會超出實際範圍導致註冊失敗。
www四個選項,我選第三個:
| 選擇 | 說明 |
|---|---|
只用 apex(example.com) |
簡潔,但 apex 不能用 CNAME(要看 DNS 商是否支援 flattening) |
只用 www |
DNS 最單純(純 CNAME) |
apex 為主 + www 301 轉址 |
使用者兩個都能用,SEO 只有一個規範網址 |
| 兩個都能訪問 | ❌ 重複內容,SEO 扣分 |
Cloudflare Pages 支援 CNAME flattening,所以 apex 可以直接指向 Pages。轉址規則用一條 redirect rule 搞定。
GitHub Pages 的 Cache-Control 不能改,讓 Day 24 的計畫落空了一半。 我原本以為「只要檔名帶內容哈希,快取策略就自動正確」。錯——檔名哈希只解決「舊檔案不被誤用」,不解決「新檔案不被積極快取」。沒有 max-age=31536000, immutable,瀏覽器每次還是會發條件請求(304),TTFB 白白多一趟。這是我把 Pages 從候選中排除的最後一根稻草。
Cloudflare Pages 的 _headers 語法錯誤靜默失敗。 縮排必須是兩個空白,而且路徑模式行不能有前導空白。寫錯的規則直接被忽略、部署照樣成功、header 就是沒有。驗證方式只能是部署後實際打:
curl -sI https://example.com/ | grep -i 'content-security\|x-content-type'
Day 25 會把這個 curl 檢查寫成自動化的上線後驗證。
AWS 的 Route 53 費用是固定的。 $0.50/月的 hosted zone 費用不在免費額度內,也不隨用量變化。很多「AWS 靜態網站免費」的教學沒提這件事。如果你只想免費,就不要用 Route 53(把 DNS 留在原本的註冊商,只用 CloudFront)。
四套都部署一次,然後用同一組檢查打它們:
for host in \
"user.github.io/repo" \
"project.pages.dev" \
"d123abc.cloudfront.net" \
"project.web.app"
do
echo "=== $host ==="
curl -sI "https://$host/index.html" | grep -iE 'HTTP/|cache-control|content-security-policy|x-content-type'
echo "TTFB: $(curl -so /dev/null -w '%{time_starttransfer}' "https://$host/index.html")s"
done
功能驗證(每一套都要跑):
chrome://serviceworker-internals)。今天的決策與理由:
上線前的五個檢查(大小寫、相對路徑、.gitignore 反查、.reference 零殘留、SW scope)看起來瑣碎,但每一條我都真的踩到過。
明天先做 CI 再做部署——順序刻意如此。沒有 CI 的自動部署,只是把 bug 推上線的自動化。 我會把 Day 10 的雙層驗證、Day 11 起累積的五支檢查腳本,全部接進 GitHub Actions,並處理 headless Chrome 在 CI 環境的兩個經典雷。